Skip to content

view: impl Serialize for generated view types (#83)#106

Open
tejas-dharani wants to merge 1 commit intoanthropics:mainfrom
tejas-dharani:feat/view-serialize
Open

view: impl Serialize for generated view types (#83)#106
tejas-dharani wants to merge 1 commit intoanthropics:mainfrom
tejas-dharani:feat/view-serialize

Conversation

@tejas-dharani
Copy link
Copy Markdown
Contributor

Closes #83.

View types had no JSON serialization path. Users holding a decoded FooView<'_> had to call .to_owned_message() and allocate a full owned message just to serialize to JSON — the whole point of views is to avoid that allocation.

Problem

generate_json already wired up #[derive(Serialize, Deserialize)] on owned message structs and enums. Views were left out because they require a manual impl rather than a derive: field access goes through getter methods, map fields need a custom iterator wrapper, and the proto3 JSON rules (omit defaults, quote int64/uint64, base64-encode bytes, encode NaN/Inf as string tokens, NullValue as JSON null) need explicit handling at codegen time. There was no generate_view_serialize function in buffa-codegen/src/view.rs.

Fix

Added generate_view_serialize in buffa-codegen/src/view.rs. When both generate_views and generate_json are enabled for a file, each generated *View<'a> struct gets a manual impl<'__a> serde::Serialize for FooView<'__a> that opens a serde map, iterates fields, and applies the same proto3 JSON rules as the owned-side impl: proto3 defaults are skipped, bytes go through json_helpers::bytes_serialize, int64/uint64 through json_helpers::int64_serialize, enum values emit their proto name, and NullValue oneof variants emit JSON null via serialize_entry(key, &()).

Map fields use a local _WM newtype that iterates the MapView and calls the appropriate key/value helpers inside a SerializeMap; the pattern mirrors the existing owned-message approach. Scalar fields needing a helper emit a local _W newtype inside a block scope — this scope is required when a message has more than one helper-typed required field (proto2), because block scoping prevents the duplicate-definition error that would otherwise result from two struct _W declarations at function scope.

OwnedView<V> gains a blanket impl<V: serde::Serialize> serde::Serialize for OwnedView<V> (cfg-gated on feature = "json") in buffa/src/view.rs, so serde_json::to_string(&owned_view) works without an explicit &*owned_view deref.

Known limitations (documented in CHANGELOG and in the generated impl's doc comment): messages whose view types nest a WKT view (TimestampView, DurationView, etc.) fail to compile because WKT view types do not yet implement Serialize. The workaround is to call .to_owned_message() and serialize the owned form; fixing the WKT side is a planned follow-up. Extension fields are also not included in view JSON output.

Tests

Four codegen integration tests cover the code-generation path: one verifies the impl serde::Serialize block is present when generate_json is enabled, one verifies it is absent when disabled, one checks that json_helpers calls appear for bytes and int64 fields, and one is a regression test for the proto2 required-field _W-collision fix (verifies the emitted token stream parses without E0428).

Nine round-trip tests in buffa-test/src/tests/json.rs exercise the generated impls end-to-end against view_json.proto (a dedicated proto file with no WKT message imports): scalar fields including int64 quoting and bytes base64, double special values (NaN/Inf/-Inf), proto3 default omission, enum name encoding, all oneof variants including NullValue, map fields with int32 keys and int64 values, nested messages with repeated fields, and the OwnedView<V> blanket impl. Each test decodes a wire-encoded message into a view and compares serde_json::to_string(&view) against serde_json::to_string(&owned) to confirm parity.

…ics#83)

When `generate_json` is enabled, each generated `*View<'a>` struct now
gets a manual `impl<'__a> serde::Serialize for FooView<'__a>` that
follows proto3-JSON semantics: proto3 defaults omitted, bytes
base64-encoded, int64/uint64 quoted, NaN/Inf as string tokens, enum
values as proto names, NullValue oneof variants as JSON null.

`OwnedView<V>` gains a blanket `impl<V: Serialize> Serialize for
OwnedView<V>` (cfg-gated on `feature = "json"`) so that
`serde_json::to_string(&owned_view)` works without an explicit deref.

Known limitations (documented in CHANGELOG and generated impl doc):
- Messages whose views nest a WKT view (TimestampView, DurationView,
  etc.) fail to compile — WKT view Serialize is a planned follow-up.
- Extension fields are not included in view JSON output.

Fixes: anthropics#83
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

View types lack serde::Serialize, forcing to_owned() for JSON

1 participant